﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;
using System.Runtime.CompilerServices;
using System.Threading.Tasks;
using Microsoft.EntityFrameworkCore;
using SW.DDD.Domain;
using static SW.DDD.Infrastructure.Utils;

namespace SW.DDD.Infrastructure
{
    public class EFRepository<TDbContext, TEntity, TKey> : IRepository<TEntity, TKey>
        where TDbContext : DbContext
        where TEntity : class, IAggregateRoot<TKey>
        where TKey : notnull
    {
        protected virtual IQueryable<TEntity> EntitiesQuery => DbContext.Set<TEntity>();

        public EFRepository(TDbContext dbContext)
        {
            DbContext = dbContext ?? throw new ArgumentNullException(nameof(dbContext));
        }

        protected TDbContext DbContext { get; set; }

        public virtual TEntity Add(TEntity entity)
        {
            if(entity == null) throw new ArgumentNullException(nameof(entity));
            return DbContext.Add(entity).Entity;
        }

        public virtual IEnumerable<TEntity> AddRange(IEnumerable<TEntity> entities)
        {
            if(entities == null) throw new ArgumentNullException(nameof(entities));
            var addRange = entities.ToList();
            DbContext.AddRange(addRange);
            return addRange;
        }

        public virtual void Delete(TEntity entity)
        {
            if(entity == null) throw new ArgumentNullException(nameof(entity));
            DbContext.Remove(entity);
        }

        public virtual void DeleteRange(IEnumerable<TEntity> entities)
        {
            if(entities == null) throw new ArgumentNullException(nameof(entities));
            DbContext.RemoveRange(entities);
        }

        public virtual void DeleteRange(Expression<Func<TEntity, bool>> whereExpression)
        {
            if(whereExpression == null) throw new ArgumentNullException(nameof(whereExpression));
            var entityQy = DbContext.Set<TEntity>().Where(whereExpression);
            DeleteRange(entityQy);
        }

        public virtual Task<TEntity?> FindByOrNullAsync(Expression<Func<TEntity, bool>> findExpression)
        {
            return EntitiesQuery.SingleOrDefaultAsync(findExpression)!;
        }

        public virtual async Task<TEntity?> FindOrNullAsync(TKey id)
        {
            if(id == null) throw new ArgumentNullException(nameof(id));
            return await EntitiesQuery.FirstOrDefaultAsync(e => e.Id.Equals(id));
        }

        public virtual async Task<IEnumerable<TView>> QueryAsync<TView>(Func<IQueryable<TEntity>, IQueryable<TView>> queryPlan) => 
            await queryPlan(EntitiesQuery.AsNoTracking()).ToListAsync();

        public async Task<int> CountAsync<TView>(Func<IQueryable<TEntity>, IQueryable<TView>> queryPlan) => 
            await queryPlan(EntitiesQuery.AsNoTracking()).CountAsync();

        public virtual async Task<TEntity> RemoveAsync(TKey id)
        {
            if(id == null) throw new ArgumentNullException(nameof(id));
            var entity = await (this as IRepository<TEntity, TKey>).FindAsync(id);
            DbContext.Remove(entity);

            return entity;
        }

        public virtual TEntity Update(TEntity entity)
        {
            if(entity == null) throw new ArgumentNullException(nameof(entity));
            DbContext.Attach(entity);
            return entity;
        }
    }
}